package com.google.code.mochaccino.framework.xml;

/*
 * Copyright 2012 Claude Houle claude.houle@gmail.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import com.google.code.mochaccino.framework.xml.stax.StAXWriter;
import com.google.code.mochaccino.framework.xml.stax.StAXWriterImpl;
import com.google.code.mochaccino.framework.xml.xpath.XPathEngine;
import com.google.code.mochaccino.framework.xml.xpath.XPathEngineException;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.net.URL;
import java.util.Properties;
import javax.inject.Inject;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.EventFilter;
import javax.xml.stream.StreamFilter;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLReporter;
import javax.xml.stream.XMLResolver;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.transform.ErrorListener;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Templates;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.URIResolver;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Attr;
import org.w3c.dom.CDATASection;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentType;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.ProcessingInstruction;
import org.w3c.dom.Text;
import org.w3c.dom.traversal.NodeIterator;
import org.xml.sax.ContentHandler;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;

/**
 * A complete XML Toolkit independant of the JAXP Implementation. So wether it is using Xerces or
 * the reference implementation from Sun, it should work.
 */
public class XMLToolkit {
	private              XPathEngine            xpath             = null;
	private static final String                 UTF_8_ENCODING    = "UTF-8";
	private static       Logger                 logger            = LoggerFactory.getLogger( XMLToolkit.class );
	private              DOMImplementation      domImplementation = null;
	private              DocumentBuilderFactory builderFactory    = null;
	private              DocumentBuilder        builder           = null;
	private              SchemaFactory          schema            = null;
	private              TransformerFactory     factory           = null;
	private              XMLInputFactory        inputFactory      = null;
	private              XMLOutputFactory       outputFactory     = null;

	/** Private Constructor */
	@Inject
	public XMLToolkit( XPathEngine engine ) {
		try {
			schema = SchemaFactory.newInstance( "http://www.w3.org/2001/XMLSchema" );
			builderFactory = DocumentBuilderFactory.newInstance();
			builder = builderFactory.newDocumentBuilder();
			domImplementation = builder.getDOMImplementation();
			inputFactory = XMLInputFactory.newInstance();
			outputFactory = XMLOutputFactory.newInstance();
			factory = TransformerFactory.newInstance();
			xpath = engine;
		} catch ( ParserConfigurationException e ) {
			throw new RuntimeException( e );
		}
	}

	/** Add Namespace */
	public void addNamespace( Document doc, QName namespace ) {
		addNamespace( doc.getDocumentElement(), namespace );
	}

	/** Add Namespace */
	public void addNamespace( Element element, QName namespace ) {
		createAttr( element, "xmlns:" + namespace.getPrefix(), namespace.getNamespaceURI() );
	}

	/** Add Namespace */
	public void addNamespace( Document doc, String namespace ) {
		addNamespace( doc.getDocumentElement(), namespace );
	}

	/** Add Namespace */
	public void addNamespace( Element element, String namespace ) {
		createAttr( element, "xmlns", namespace );
	}

	/** Add Namespace */
	public void addNamespace( Document doc, String prefix, String namespace ) {
		addNamespace( doc.getDocumentElement(), prefix, namespace );
	}

	/** Add Namespace */
	public void addNamespace( Element element, String prefix, String namespace ) {
		createAttr( element, "xmlns:" + prefix, namespace );
	}

	/** Add Processing Instruction */
	public ProcessingInstruction addProcessingInstruction( Document doc, String target, String data ) {
		ProcessingInstruction instruction = doc.createProcessingInstruction( target, data );
		doc.insertBefore( instruction, doc.getDocumentElement() );
		return instruction;
	}

	/** Add Attribute */
	public Attr createAttr( Element element, String name, String value ) {
		if ( element == null ) {
			return null;
		}
		if ( name == null ) {
			return null;
		}
		if ( value == null ) {
			return null;
		}
		Document doc = element.getOwnerDocument();
		Attr attr = doc.createAttribute( name );
		element.setAttributeNode( attr );
		attr.setValue( value );
		return attr;
	}

	/** Add Attribute */
	public <T> Attr createAttr( Element element, String name, T value ) {
		return createAttr( element, name, TranscodingUtil.encode( value ) );
	}

	/** Add Text Node */
	public CDATASection createCDATA( Element element, String value ) {
		if ( element == null ) {
			return null;
		}
		if ( value == null ) {
			return null;
		}
		Document doc = element.getOwnerDocument();
		CDATASection cdata = doc.createCDATASection( value );
		element.appendChild( cdata );
		return cdata;
	}

	/** Add CDATASection Node */
	public <T> CDATASection createCDATA( Element element, T value ) {
		return createCDATA( element, TranscodingUtil.encode( value ) );
	}

	/** Add Element with Value */
	public <T> Element createCDATAElement( Element parent, String tag, T value ) {
		return createCDATAElement( parent, tag, TranscodingUtil.encode( value ) );
	}

	/** Add Element with Value */
	public Element createCDATAElement( Element parent, String tag, String value ) {
		if ( (value != null) && (value.length() > 0) ) {
			Element elem = createElement( parent, tag );
			if ( elem != null ) {
				createCDATA( elem, value );
			}
			return elem;
		}
		return null;
	}

	/** Add Element */
	public Element createElement( Element element, String name ) {
		if ( element != null ) {
			Document doc = element.getOwnerDocument();
			Element elem = doc.createElement( name );
			element.appendChild( elem );
			return elem;
		}
		return null;
	}

	/** Create New Document */
	public Document createNewDocument( String name ) {
		return createNewDocument( null, name );
	}

	/** Create New Document */
	public Document createNewDocument( String namespace, String name ) {
		return createNewDocument( namespace, name, null );
	}

	/** Create New Document */
	public Document createNewDocument( String namespace, String name, DocumentType docType ) {
		return domImplementation.createDocument( namespace, name, docType );
	}

	/** Get Source */
	public Templates createTemplate( byte[] in ) throws TransformerConfigurationException {
		return createTemplate( getSource( in ) );
	}

	/** Get Source */
	public Source getSource( byte[] source ) {
		return getSource( new ByteArrayInputStream( source ) );
	}

	/** Get Source */
	public Templates createTemplate( File in ) throws TransformerConfigurationException {
		return createTemplate( getSource( in ) );
	}

	/** Get Source */
	public Source getSource( File file ) {
		return new StreamSource( file );
	}

	/** Get Source */
	public Templates createTemplate( InputSource source ) throws TransformerConfigurationException {
		return createTemplate( getSource( source ) );
	}

	/** Get Source */
	public Source getSource( InputSource node ) {
		return new SAXSource( node );
	}

	/** Get Source */
	public Templates createTemplate( InputStream in ) throws TransformerConfigurationException {
		return createTemplate( getSource( in ) );
	}

	/** Get Source */
	public Templates createTemplate( Node node ) throws TransformerConfigurationException {
		return createTemplate( getSource( node ) );
	}

	/** Get Source */
	public Templates createTemplate( Node node, String sysid ) throws TransformerConfigurationException {
		return createTemplate( getSource( node, sysid ) );
	}

	/** Get Source */
	public Source getSource( Node node, String sysid ) {
		return new DOMSource( node, sysid );
	}

	/** Get Source */
	public Templates createTemplate( Reader in ) throws TransformerConfigurationException {
		return createTemplate( getSource( in ) );
	}

	/** Get Source */
	public Source getSource( Reader reader ) {
		return new StreamSource( new BufferedReader( reader ) );
	}

	/** Create Templates */
	public Templates createTemplate( Source source ) throws TransformerConfigurationException {
		return factory.newTemplates( source );
	}

	/** Get Source */
	public Templates createTemplate( XMLReader node, InputSource source ) throws TransformerConfigurationException {
		return createTemplate( getSource( node, source ) );
	}

	/** Get Source */
	public Source getSource( XMLReader node, InputSource input ) {
		return new SAXSource( node, input );
	}

	/** Add Text Node */
	public Text createText( Element element, String value ) {
		if ( element == null ) {
			return null;
		}
		if ( value == null ) {
			return null;
		}
		Document doc = element.getOwnerDocument();
		Text txt = doc.createTextNode( value );
		element.appendChild( txt );
		return txt;
	}

	/** Add Text Node */
	public <T> Text createText( Element element, T value ) {
		return createText( element, TranscodingUtil.encode( value ) );
	}

	/** Add Element with Value */
	public <T> Element createTextElement( Element parent, String tag, T value ) {
		return createTextElement( parent, tag, TranscodingUtil.encode( value ) );
	}

	/** Add Element with Value */
	public Element createTextElement( Element parent, String tag, String value ) {
		if ( (value != null) && (value.length() > 0) ) {
			Element elem = createElement( parent, tag );
			if ( elem != null ) {
				createText( elem, value );
			}
			return elem;
		}
		return null;
	}

	/** Create Transformer */
	public Transformer createTransformer() throws TransformerConfigurationException {
		return createTransformer( new XSLErrorListener() );
	}

	/** Create Transformer */
	public Transformer createTransformer( ErrorListener listener ) throws TransformerConfigurationException {
		Transformer trans = factory.newTransformer();
		trans.setOutputProperty( "indent", "yes" );
		trans.setErrorListener( listener );
		return trans;
	}

	/** Create Transformer */
	public Transformer createTransformer( ErrorListener listener, Properties outputProperties ) throws TransformerConfigurationException {
		Transformer trans = createTransformer();
		trans.setErrorListener( listener );
		trans.setOutputProperties( outputProperties );
		return trans;
	}

	/** Create Transformer */
	public Transformer createTransformer( ErrorListener listener, Properties outputProperties, URIResolver result ) throws TransformerConfigurationException {
		Transformer trans = createTransformer();
		trans.setErrorListener( listener );
		trans.setOutputProperties( outputProperties );
		trans.setURIResolver( result );
		return trans;
	}

	/** Create Transformer From Template */
	public Transformer createTransformer( Templates template ) throws TransformerConfigurationException {
		return createTransformer( template, new XSLErrorListener() );
	}

	/** Create Transformer From Template */
	public Transformer createTransformer( Templates template, ErrorListener listener ) throws TransformerConfigurationException {
		Transformer trans = template.newTransformer();
		trans.setOutputProperty( "indent", "yes" );
		trans.setErrorListener( listener );
		return trans;
	}

	/** Create Transformer From Template */
	public Transformer createTransformer( Templates template, ErrorListener listener, Properties outputProperties ) throws TransformerConfigurationException {
		Transformer trans = createTransformer( template, listener );
		trans.setOutputProperties( outputProperties );
		return trans;
	}

	/** Create Transformer From Template */
	public Transformer createTransformer( Templates template, ErrorListener listener, Properties outputProperties, URIResolver uriResolver ) throws TransformerConfigurationException {
		Transformer trans = createTransformer( template, listener );
		trans.setOutputProperties( outputProperties );
		trans.setURIResolver( uriResolver );
		return trans;
	}

	/** Get Attribute */
	public String getAttributeValue( Node node, String name ) {
		return getAttributeValue( node, name, null );
	}

	/** Get Attribute */
	public String getAttributeValue( Node node, String name, String defaultValue ) {
		Attr attr = getAttribute( node, name );
		if ( attr == null ) {
			return defaultValue;
		}
		String val = attr.getValue();
		if ( val == null ) {
			return defaultValue;
		}
		return val;
	}

	/** Get Attribute */
	private Attr getAttribute( Node element, String name ) {
		if ( element != null ) {
			return (Attr) element.getAttributes().getNamedItem( name );
		}
		return null;
	}

	/** Get Attribute Value As */
	public <T> T getAttributeValueAs( Node element, String name, Class<T> type ) {
		return TranscodingUtil.decode( getAttributeValue( element, name ), type, null );
	}

	/** Get Attribute Value As */
	public <T> T getAttributeValueAs( Node element, String name, Class<T> type, T defaultValue ) {
		return TranscodingUtil.decode( getAttributeValue( element, name ), type, defaultValue );
	}

	/** Get a Default Error Listener */
	public ErrorListener getDefaultErrorListener() {
		return new XSLErrorListener();
	}

	/** Get Element Text */
	public String getElementText( Node element ) {
		return getElementText( element, (String) null );
	}

	/** Get Element Text */
	public String getElementText( Node element, String defaultValue ) {
		Text txt = getText( element );
		if ( txt == null ) {
			return defaultValue;
		}
		return txt.getData();
	}

	/** Get Element Text */
	private Text getText( Node element ) {
		if ( element != null ) {
			for ( Node child = element.getFirstChild(); child != null; child = child.getNextSibling() ) {
				if ( child.getNodeType() == Node.TEXT_NODE ) {
					return (Text) child;
				}
				if ( child.getNodeType() == Node.CDATA_SECTION_NODE ) {
					return (Text) child;
				}
			}
		}
		return null;
	}

	/** Get Element Text\ */
	public <T> T getElementText( Node element, Class<T> type ) {
		return getElementText( element, type, null );
	}

	/** Get Element Text */
	public <T> T getElementText( Node element, Class<T> type, T defaultValue ) {
		Text txt = getText( element );
		if ( txt == null ) {
			return defaultValue;
		}
		return TranscodingUtil.decode( txt.getData(), type, defaultValue );
	}

	/** Get Result */
	public Result getResult( Node result ) {
		return new DOMResult( result );
	}

	/** Get Result */
	public Result getResult( Node result, String id ) {
		return new DOMResult( result, id );
	}

	/** Get Schema */
	public Schema getSchema( File pFile ) throws SAXException {
		return schema.newSchema( pFile );
	}

	/** Get Schema */
	public Schema getSchema( Source pFile ) throws SAXException {
		return schema.newSchema( pFile );
	}

	/** Get Schema */
	public Schema getSchema( Source[] pFile ) throws SAXException {
		return schema.newSchema( pFile );
	}

	/** Get Schema */
	public Schema getSchema( URL pFile ) throws SAXException {
		return schema.newSchema( pFile );
	}

	/** Get Source */
	public Source getSource( InputStream input ) {
		return new StreamSource( new BufferedInputStream( input ) );
	}

	/** Get Source */
	public Source getSource( Node node ) {
		return new DOMSource( node );
	}

	/** Get StAX Writer */
	public StAXWriter getStAXWriter( OutputStream out ) throws XMLStreamException {
		return new StAXWriterImpl( outputFactory.createXMLStreamWriter( out ) );
	}

	/** Get StAX Writer */
	public StAXWriter getStAXWriter( OutputStream out, String encoding ) throws XMLStreamException {
		return new StAXWriterImpl( outputFactory.createXMLStreamWriter( out, encoding ) );
	}

	/** Get StAX Writer */
	public StAXWriter getStAXWriter( Result out ) throws XMLStreamException {
		return new StAXWriterImpl( outputFactory.createXMLStreamWriter( out ) );
	}

	/** Get StAX Writer */
	public StAXWriter getStAXWriter( Writer out ) throws XMLStreamException {
		return new StAXWriterImpl( outputFactory.createXMLStreamWriter( out ) );
	}

	/** Get a XML Reader */
	public XMLEventReader getXMLEventReader( InputStream in ) throws XMLStreamException {
		return inputFactory.createXMLEventReader( in );
	}

	/** Get a XML Reader */
	public XMLEventReader getXMLEventReader( InputStream in, String encoding ) throws XMLStreamException {
		return inputFactory.createXMLEventReader( in, encoding );
	}

	/** Get a XML Reader */
	public XMLEventReader getXMLEventReader( Reader in ) throws XMLStreamException {
		return inputFactory.createXMLEventReader( in );
	}

	/** Get a XML Reader */
	public XMLEventReader getXMLEventReader( Source in ) throws XMLStreamException {
		return inputFactory.createXMLEventReader( in );
	}

	/** Get a XML Reader */
	public XMLEventReader getXMLEventReader( XMLEventReader in, EventFilter filter ) throws XMLStreamException {
		return inputFactory.createFilteredReader( in, filter );
	}

	/** Get a XML Reader */
	public XMLStreamReader getXMLStreamReader( InputStream in ) throws XMLStreamException {
		return inputFactory.createXMLStreamReader( in );
	}

	/** Get a XML Reader */
	public XMLStreamReader getXMLStreamReader( InputStream in, String encoding ) throws XMLStreamException {
		return inputFactory.createXMLStreamReader( in, encoding );
	}

	/** Get a XML Reader */
	public XMLStreamReader getXMLStreamReader( Reader in ) throws XMLStreamException {
		return inputFactory.createXMLStreamReader( in );
	}

	/** Get a XML Reader */
	public XMLStreamReader getXMLStreamReader( Source in ) throws XMLStreamException {
		return inputFactory.createXMLStreamReader( in );
	}

	/** Get a XML Reader */
	public XMLStreamReader getXMLStreamReader( XMLStreamReader in, StreamFilter filter ) throws XMLStreamException {
		return inputFactory.createFilteredReader( in, filter );
	}

	/** Import Node */
	public Node importNode( Node parent, Node node ) {
		return importNode( parent, node, true );
	}

	/** Import Node */
	public Node importNode( Node parent, Node node, boolean deep ) {
		if ( (parent != null) && (node != null) ) {
			Document doc = parent.getOwnerDocument();
			Node importedNode = doc.importNode( node, deep );
			parent.appendChild( importedNode );
			return importedNode;
		}
		return null;
	}

	/** Parse Document */
	public Document parseDocument( InputSource source ) throws IOException, SAXException {
		try {
			DocumentBuilder documentBuilder = builderFactory.newDocumentBuilder();
			return documentBuilder.parse( source );
		} catch ( ParserConfigurationException e ) {
			logger.error( e.getMessage(), e );
			return null;
		}
	}

	/** Parse Document */
	public Document parseDocument( InputStream in ) throws IOException, SAXException {
		InputSource source = new InputSource( in );
		source.setEncoding( UTF_8_ENCODING );
		return parseDocument( source );
	}

	/** Parse Document */
	public Document parseDocument( String xml ) throws IOException, SAXException {
		return parseDocument( new StringReader( xml ) );
	}

	/** Parse Document */
	public Document parseDocument( Reader in ) throws SAXException, IOException {
		return parseDocument( new InputSource( in ) );
	}

	/** Select Nodes */
	public Node selectNode( Node node, String xpathExpression ) throws XPathEngineException {
		return xpath.selectNode( node, xpathExpression );
	}

	/** Select Nodes */
	public NodeIterator selectNodeIterator( Node node, String xpathExpression ) throws XPathEngineException {
		return xpath.selectNodeIterator( node, xpathExpression );
	}

	/** Select Nodes */
	public NodeList selectNodeList( Node node, String xpathExpression ) throws XPathEngineException {
		return xpath.selectNodeList( node, xpathExpression );
	}

	/** Serialize */
	public void serialize( Node node, ContentHandler output ) throws TransformerException {
		transform( createTransformer(), getSource( node ), getResult( output ) );
	}

	/** Get Result */
	public Result getResult( ContentHandler pHandler ) {
		return new SAXResult( pHandler );
	}

	/** Serialize */
	public void serialize( Node node, File output ) throws TransformerException {
		transform( createTransformer(), getSource( node ), getResult( output ) );
	}

	/** Get Result */
	public Result getResult( File result ) {
		return new StreamResult( result );
	}

	/** Serialize as Byte Array */
	public byte[] serializeAsBytes( Node node ) throws TransformerConfigurationException, TransformerException {
		ByteArrayOutputStream out = new ByteArrayOutputStream();
		serialize( node, out );
		IOUtils.closeQuietly( out );
		return out.toByteArray();
	}

	/** Serialize */
	public void serialize( Node node, OutputStream output ) throws TransformerException {
		transform( createTransformer(), getSource( node ), getResult( output ) );
	}

	/** Get Result */
	public Result getResult( OutputStream result ) {
		return new StreamResult( new BufferedOutputStream( result ) );
	}

	/** Serialize as String */
	public String serializeAsString( Node node ) throws TransformerConfigurationException, TransformerException {
		StringWriter out = new StringWriter();
		serialize( node, out );
		IOUtils.closeQuietly( out );
		return out.toString();
	}

	/** Serialize */
	public void serialize( Node node, Writer output ) throws TransformerException {
		transform( createTransformer(), getSource( node ), getResult( output ) );
	}

	/** Get Result */
	public Result getResult( Writer result ) {
		return new StreamResult( new BufferedWriter( result ) );
	}

	/** Set Property */
	public void setXMLInputFactoryProperty( String key, Object value ) {
		inputFactory.setProperty( key, value );
	}

	/** Set Property */
	public void setXMLOutputFactoryProperty( String key, Object value ) {
		outputFactory.setProperty( key, value );
	}

	/** Set XML Reporter */
	public void setXMLReporter( XMLReporter reporter ) {
		inputFactory.setXMLReporter( reporter );
	}

	/** Set XML Resolver */
	public void setXMLResolver( XMLResolver resolver ) {
		inputFactory.setXMLResolver( resolver );
	}

	/** Transform */
	public void transform( Transformer transformer, Source source, Result result ) throws TransformerConfigurationException, TransformerException {
		transformer.setOutputProperty( OutputKeys.INDENT, "yes" );
		transformer.setOutputProperty( "{http://xml.apache.org/xslt}indent-amount", "4" );
		transformer.transform( source, result );
	}

	/** Validate */
	public void validate( Schema schema, Source source, Result result ) throws SAXException, IOException {
		Validator validator = schema.newValidator();
		validator.validate( source, result );
	}

	/** Validate */
	public void validate( Schema schema, Source src, Result result, ErrorHandler handler ) throws SAXException, IOException {
		Validator validator = schema.newValidator();
		validator.setErrorHandler( handler );
		validator.validate( src, result );
	}

	protected static class XSLErrorListener implements ErrorListener {
		private Logger log = LoggerFactory.getLogger( getClass() );

		@Override
		public void error( TransformerException ex ) throws TransformerException {
			log.error( ex.getMessage(), ex );
		}

		@Override
		public void fatalError( TransformerException ex ) throws TransformerException {
			log.error( ex.getMessage(), ex );
			throw ex;
		}

		@Override
		public void warning( TransformerException ex ) throws TransformerException {
			log.warn( ex.getMessage(), ex );
		}
	}
}